/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * You may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { ArkClass, ArkMethod, Stmt } from 'arkanalyzer';
import { ArkInstanceInvokeExpr } from 'arkanalyzer';
import { BaseChecker, BaseMetaData } from '../BaseChecker';
import { Rule, Defects, MatcherTypes, ClassMatcher, MatcherCallback, CheckerUtils } from '../../Index';
import { IssueReport } from '../../model/Defects';

// 常量提取
const ON_PAGE_HIDE_METHOD = 'onPageHide';
const SET_TIMEOUT_METHOD = 'setTimeout';
const RELEASE_METHOD_PREFIX = 'release';
// 预定义的资源释放方法列表
const RELEASE_METHODS = ['stop', 'release', 'close', 'destroy', 'dispose', 'free'];
// 预定义的资源对象名列表
const RESOURCE_OBJECTS = ['captureSession', 'cameraInput', 'previewOutput', 'cameraOutput'];
// 预定义的资源释放方法名列表（包含完整方法名）
const RELEASE_METHOD_NAMES = ['releaseCamera', 'releaseOutput', 'releaseOther'];
const RULE_DOC_PATH = 'docs/on-page-hide-release-check.md';
const RULE_DESCRIPTION = 'In the onPageHide method, resource release should be delayed; otherwise, it will lead to a decrease in page efficiency.';

const gMetaData: BaseMetaData = {
    severity: 2,
    ruleDocPath: RULE_DOC_PATH,
    description: RULE_DESCRIPTION
};

export class OnPageHideReleaseCheck implements BaseChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public issues: IssueReport[] = [];

    private clsMatcher: ClassMatcher = {
        matcherType: MatcherTypes.CLASS
    };

    public registerMatchers(): MatcherCallback[] {
        return [{ matcher: this.clsMatcher, callback: this.check }];
    }

    public check = (target: ArkClass): void => {
        for (const method of target.getMethods()) {
            const methodName = method.getSignature().getMethodSubSignature().getMethodName();
            if (methodName !== ON_PAGE_HIDE_METHOD) { 
                continue; 
            }
            this.checkOnPageHideMethod(target, method);
        }
    };

    private checkOnPageHideMethod(target: ArkClass, method: ArkMethod): void {
        const stmts = method.getBody()?.getCfg().getStmts() ?? [];
        for (const stmt of stmts) {
            const invokeExpr = CheckerUtils.getInvokeExprFromStmt(stmt);
            if (!invokeExpr) { 
                continue; 
            }
            const calledMethodName = invokeExpr.getMethodSignature().getMethodSubSignature().getMethodName();

            if (this.isDelayedReleaseBySetTimeout(invokeExpr, target)) {
                continue;
            }

            if (this.shouldReportReleaseCall(invokeExpr, calledMethodName, target)) {
                this.reportIssue(target, stmt, calledMethodName);
            }
        }
    }

    // 判断 setTimeout 包裹的资源释放
    private isDelayedReleaseBySetTimeout(invokeExpr: any, target: ArkClass): boolean {
        const calledMethodName = invokeExpr.getMethodSignature().getMethodSubSignature().getMethodName();
        if (calledMethodName !== SET_TIMEOUT_METHOD) { 
            return false; 
        }
        const args = invokeExpr.getArgs();
        if (args.length > 0) {
            const arg0 = args[0];
            if (this.isReleaseMethodArg(arg0, target)) {
                return true;
            }
        }
        return false;
    }

    // 判断是否应报告资源释放调用
    private shouldReportReleaseCall(invokeExpr: any, calledMethodName: string, target: ArkClass): boolean {
        let callerName = '';
        if (invokeExpr instanceof ArkInstanceInvokeExpr) {
            callerName = invokeExpr.getBase().getName();
        }
        return this.isReleaseMethodCall(calledMethodName, callerName, target);
    }

    // 判断是否为资源对象
    private isResourceObject(callerName: string): boolean {
        return RESOURCE_OBJECTS.includes(callerName);
    }

    // 判断参数是否为资源释放方法
    private isReleaseMethodArg(arg: any, target: ArkClass): boolean {
        if (arg && typeof arg.getName === 'function') {
            const name = arg.getName();
            if (typeof name === 'string') {
                if (this.isPredefinedReleaseMethodName(name)) {
                    return true;
                }
                if (this.isResourceObjectReleaseMethod(arg, name)) {
                    return true;
                }
            }
        }
        return false;
    }

    // 判断是否为预定义的资源释放方法名
    private isPredefinedReleaseMethodName(name: string): boolean {
        return RELEASE_METHOD_NAMES.includes(name);
    }

    // 判断是否为资源对象的释放方法
    private isResourceObjectReleaseMethod(arg: any, name: string): boolean {
        if (typeof arg.getOwner === 'function') {
            const owner = arg.getOwner();
            if (owner && typeof owner.getName === 'function') {
                const ownerName = owner.getName();
                if (RELEASE_METHODS.includes(name) && this.isResourceObject(ownerName)) {
                    return true;
                }
            }
        }
        return false;
    }

    // 判断是否为资源释放方法调用
    private isReleaseMethodCall(methodName: string, callerName: string, target: ArkClass): boolean {
        // 检查是否为预定义的资源释放方法名
        if (RELEASE_METHOD_NAMES.includes(methodName)) {
            return true;
        }
        // 检查是否为资源对象的释放方法调用
        if (RELEASE_METHODS.includes(methodName) && this.isResourceObject(callerName)) {
            return true;
        }
        return false;
    }

    private reportIssue(target: ArkClass, stmt: Stmt, methodName: string): void {
        const text = stmt.getOriginalText();
        if (!text || text.length === 0) { 
            return; 
        }
        const index = text.indexOf(methodName);
        if (index === -1) { 
            return; 
        }
        const severity = this.rule.alert ?? this.metaData.severity;
        const originalPosition = stmt.getOriginPositionInfo();
        const lineNum = originalPosition.getLineNo();
        const startColum = originalPosition.getColNo() + index;
        const endColum = startColum + methodName.length - 1;
        const filePath = target.getDeclaringArkFile().getFilePath();
        let defect = new Defects(
            lineNum, startColum, endColum, this.metaData.description, severity, this.rule.ruleId, filePath, this.metaData.ruleDocPath, true, false, false);
        this.issues.push(new IssueReport(defect, undefined));
    }
}
